package k8s

import (
	"context"
	"fmt"
	"strings"

	"v8s-controller/internal"
	"v8s/logger"

	"go.uber.org/zap"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer/json"
	"k8s.io/client-go/kubernetes/scheme"

	"github.com/google/go-cmp/cmp"
)

type (
	Repository struct {
		log *zap.SugaredLogger

		serializer *json.Serializer
		client     Client
	}

	Namespace struct {
		ID internal.Namespace
	}
)

const (
	IndexedSecretName     = "resource-index"
	IndexedSecretDataName = "resource-array"
)

func CreateRepository(client Client) *Repository {
	serializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{
		Yaml:   true,
		Strict: true,
	})

	return &Repository{
		log: logger.Logger().Named("k8s-repository").Sugar(),

		serializer: serializer,
		client:     client,
	}
}

func (repository *Repository) ListNotDeletedNamespaces(ctx context.Context) ([]Namespace, error) {
	namespaces, err := ListNamespaces(ctx, repository.client)
	if err != nil {
		return nil, fmt.Errorf("failed to request namespaces from kubernetes: %w", err)
	}

	projects := make([]Namespace, 0, len(namespaces))

	for _, namespace := range namespaces {
		if namespace.DeletionTimestamp != nil {
			continue
		}

		projects = append(projects, Namespace{
			ID: internal.Namespace(namespace.GetName()),
		})
	}

	return projects, nil
}

func (repository *Repository) ListNamespaces(ctx context.Context) ([]Namespace, error) {
	namespaces, err := ListNamespaces(ctx, repository.client)
	if err != nil {
		return nil, fmt.Errorf("failed to request namespaces from kubernetes: %w", err)
	}

	projects := make([]Namespace, 0, len(namespaces))

	for _, namespace := range namespaces {
		projects = append(projects, Namespace{
			ID: internal.Namespace(namespace.GetName()),
		})
	}

	return projects, nil
}

func (repository *Repository) CreateResources(ctx context.Context, resources []runtime.Object) error {
	describable, err := ToDescribableResources(resources)
	if err != nil {
		return fmt.Errorf("failed to convert to describable resources: %w", err)
	}

	var namespace *DescribableResource
	for _, resource := range describable {
		if resource.IsKind(NamespaceKind) {
			if namespace != nil {
				return fmt.Errorf("only one namespace is supported for creation, but found '%s', '%s'", resource.Name(), namespace.Name())
			}

			namespace = resource
		}
	}

	if namespace == nil {
		return fmt.Errorf("namespace not found in resources")
	}

	name := internal.Namespace(namespace.Name())
	log := repository.log.With("namespace", name)

	log.Debugf("creating namespace")

	if err := CreateResource(ctx, repository.client, name, namespace); err != nil {
		return fmt.Errorf("failed to create namespace '%s': %w", name, err)
	}

	log.Debugf("created namespace")

	for _, resource := range describable {
		if resource.IsKind(NamespaceKind) {
			log.Debugf("namespace is already created, skipping")
			continue
		}

		log.Debugf("creating resource '%s'", resource.Identity())

		if err := CreateResource(ctx, repository.client, name, resource); err != nil {
			return err
		}

		log.Debugf("'%s' created", resource.Identity())
	}

	index, err := repository.convertToResourceIndex(describable)
	if err != nil {
		return fmt.Errorf("failed to convert to resource index: %w", err)
	}

	log.Debugf("'%d' resources created", len(resources)-1)

	return CreateResource(ctx, repository.client, name, index)
}

func (repository *Repository) UpdateResources(ctx context.Context, namespace internal.Namespace, resources []runtime.Object) (bool, error) {
	repository.log.Debugf("updating resources in namespace '%s'", namespace)
	recreated := 0
	updated := 0
	created := 0
	deleted := 0

	existing, err := getExistingResourcesIndex(ctx, repository.client, namespace)
	if err != nil {
		return false, fmt.Errorf("failed to get existing resources: %w", err)
	}

	describable, err := ToDescribableResources(resources)
	if err != nil {
		return false, fmt.Errorf("failed to convert to describable resources: %w", err)
	}

	for _, resource := range describable {
		existed := existing.RemoveResource(resource)

		if resource.IsKind(NamespaceKind) {
			repository.log.Debugf("resource '%s' will not be updated", resource.Identity())
			continue
		}

		if existed == nil {
			repository.log.Debugf("creating resource '%s' in namespace '%s'", resource.Identity(), namespace)

			if err := CreateResource(ctx, repository.client, namespace, resource); err != nil {
				return false, fmt.Errorf("failed to create resource '%s' : %w", resource.Identity(), err)
			}

			created++
			repository.log.Infof("namespace '%s': resource '%s' created ", namespace, resource.Identity())

			continue
		}

		if !shouldUpdate(existed, resource) {
			repository.log.Debugf("resource '%s' completly exists in namespace '%s', skipping", resource.Identity(), namespace)

			continue
		}

		repository.log.Debugf("updating resource '%s' in namespace '%s'", resource.Identity(), namespace)

		err = UpdateResource(ctx, repository.client, namespace, resource)
		if err != nil {
			if shouldCreateAfterUpdateFailure(err) {
				repository.log.Debugf("creating resource '%s' in namespace '%s' after update failure", resource.Identity(), namespace)

				if err := CreateResource(ctx, repository.client, namespace, resource); err != nil {
					return false, fmt.Errorf("failed to create resource '%s' after update failure: %w", resource.Identity(), err)
				}

				created++
				repository.log.Infof("namespace '%s': resource '%s' created after update failure", namespace, resource.Identity())

				continue
			}

			if shouldRecreate(err, resource) {
				repository.log.Debugf("recreating resource '%s' in namespace '%s'", resource.Identity(), namespace)

				if err := DeleteResource(ctx, repository.client, namespace, resource); err != nil {
					return false, fmt.Errorf("failed to delete resource '%s': %w", resource.Identity(), err)
				}

				if err := CreateResource(ctx, repository.client, namespace, resource); err != nil {
					return false, fmt.Errorf("failed to recreate resource '%s': %w", resource.Identity(), err)
				}

				recreated++
				repository.log.Infof("namespace '%s': resource '%s' recreated", namespace, resource.Identity())

				continue
			}

			return false, fmt.Errorf("failed to update resource '%s' : %w", resource.Identity(), err)
		}

		updated++
		repository.log.Infof("namespace '%s': resource '%s' updated", namespace, resource.Identity())
	}

	for identity, resource := range existing {
		repository.log.Debugf("resource '%s' no longer present in namespace '%s', deleting", identity, namespace)

		if err := DeleteResource(ctx, repository.client, namespace, resource); err != nil {
			return false, fmt.Errorf("failed to delete resource '%s' in namespace '%s'", identity, namespace)
		}

		deleted++
		repository.log.Infof("namespace '%s': resource '%s' deleted", namespace, identity)
	}

	index, err := repository.convertToResourceIndex(describable)
	if err != nil {
		return false, fmt.Errorf("failed to convert resources to index: %w", err)
	}

	if err = UpdateResource(ctx, repository.client, namespace, index); err != nil {
		return false, fmt.Errorf("failed to save resource index: %w", err)
	}

	repository.log.With("update", fmt.Sprintf(`{
  "updated": %d,
  "created": %d,
  "recreated": %d,
  "deleted": %d
}`, updated, created, recreated, deleted)).Debugf("resources updated in namespace '%s'", namespace)

	return updated != 0 || created != 0 || recreated != 0 || deleted != 0, nil
}

func (repository *Repository) DeleteNamespace(ctx context.Context, namespace internal.Namespace) error {
	repository.log.Debugf("deleting namespace '%s'", namespace)

	if err := DeleteNamespace(ctx, repository.client, namespace); err != nil {
		return fmt.Errorf("failed to delete namespace '%s': %w", namespace, err)
	}

	repository.log.Debugf("namespace '%s' deleted", namespace)

	return nil
}

func shouldUpdate(existed *DescribableResource, resource *DescribableResource) bool {
	options := cmp.Options{}

	should := !cmp.Equal(existed.object, resource.object, options...)

	if should {
		diff := cmp.Diff(existed.object, resource.object, options...)
		fmt.Printf(diff + "\n")
	}

	return should
}

func shouldRecreate(err error, resource *DescribableResource) bool {
	if errors.IsInvalid(err) {
		return true
	}

	if resource.IsKind(ServiceKind) && errors.IsForbidden(err) {
		return true
	}

	return false
}

func shouldCreateAfterUpdateFailure(err error) bool {
	return errors.IsNotFound(err)
}

func (repository *Repository) convertToResourceIndex(describable DescribableResources) (*DescribableResource, error) {
	builder := &strings.Builder{}

	for index, resource := range describable {
		if index != 0 {
			builder.WriteString("---\n")
		}

		if err := repository.serializer.Encode(resource.object, builder); err != nil {
			return nil, fmt.Errorf("failed to serialize '%s': %w", resource.Identity(), err)
		}
	}

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: IndexedSecretName,
		},
		StringData: map[string]string{
			IndexedSecretDataName: builder.String(),
		},
		Type: corev1.SecretTypeOpaque,
	}

	resource, err := ToDescribableResource(secret)
	if err != nil {
		return nil, fmt.Errorf("failed to convert secret to describable: %w", err)
	}

	return resource, nil
}

func getExistingResourcesIndex(ctx context.Context, client Client, namespace internal.Namespace) (IndexedDescribableResources, error) {
	resource, err := GetResource(ctx, client, namespace, SecretKind, IndexedSecretName)
	if err != nil {
		return nil, fmt.Errorf("failed to get secret '%s' in namespace '%s': %w", IndexedSecretName, namespace, err)
	}

	secret := resource.object.(*corev1.Secret)

	data, exists := secret.StringData[IndexedSecretDataName]
	if !exists {
		plain, plainExists := secret.Data[IndexedSecretDataName]
		if !plainExists {
			return nil, fmt.Errorf("secret '%s' do not have data '%s'", IndexedSecretName, IndexedSecretDataName)
		}

		data = string(plain)
	}

	split := strings.Split(data, "---")
	objects := make([]runtime.Object, 0, len(split))

	for index, resourceFromSplit := range split {
		object, _, err := scheme.Codecs.UniversalDeserializer().Decode([]byte(resourceFromSplit), nil, nil)
		if err != nil {
			return nil, fmt.Errorf("failed to deserialize '%d' from secret '%s': %w", index, IndexedSecretName, err)
		}

		objects = append(objects, object)
	}

	resources, err := ToDescribableResources(objects)
	if err != nil {
		return nil, fmt.Errorf("failed to convert to describable from secret '%s': %w", IndexedSecretName, err)
	}

	return resources.Indexed(), nil
}
